﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace M3G {

    public class Camera : Node {

        #region Constant
        public const int Generic     = 48;
        public const int Parallel    = 49;
        public const int Perspective = 50;
        public const int Screen      = 51;
        #endregion

        #region Field
        int       mode;
        float     fovy;
        float     aspectRation;
        float     near;
        float     far;
        float     x;
        float     y;
        float     width;
        float     height;
        Transform generic;
        #endregion

        #region Method
        public Camera () {
            this.mode = Camera.Generic;
            this.fovy = 0;
            this.aspectRation = 0;
            this.near = 0;
            this.far = 0;
            this.generic = new Transform ();
        }

        protected Camera (Camera camera)
            : base (camera) {
            this.mode = camera.mode;
            this.fovy = camera.fovy;
            this.aspectRation = camera.aspectRation;
            this.near = camera.near;
            this.far = camera.far;
            this.generic = new Transform (camera.generic);
        }

        public int Mode {
            get { return mode; }
        }

        public float[] ProjectionParameters {
            get {
                switch (mode) {
                    case Camera.Perspective: return new float[] { fovy, aspectRation, near, far };
                    case Camera.Parallel: return new float[] { fovy, aspectRation, near, far };
                    case Camera.Screen: return new float[] { x, y, width, height };
                    case Camera.Generic: return new float[] { };
                    default: return null;
                }
            }
        }


        public Transform ProjectionTransform {
            get {
                var projection = new Transform ();
                switch (mode) {
                    case Camera.Parallel: {
                        var h = fovy;
                        var w = aspectRation * h;
                        var d = far - near;
                        projection.Matrix = new float[] {2/w,   0,    0,  0, 
                                                           0, 2/h,    0,  0,
                                                           0,   0, -2/d, -(near+far)/d,
                                                           0,   0,    0,  1};
                        return projection;
                    }
                    case Camera.Perspective: {
                        var h = (float)Math.Tan (fovy / 2.0f);
                        var w = aspectRation * h;
                        var d = far - near;
                        projection.Matrix = new float[]{1/w,   0,  0           ,  0,
                                                              0, 1/h,  0           ,  0,
                                                              0,   0, -(near+far)/d, -2*near*far/d,
                                                              0,   0, -1           ,  0};
                        return projection;
                    }
                    case Camera.Screen: {
                        var w = width;
                        var h = height;
                        var tx = -(2 * x + width) / width;
                        var ty = -(2 * y + height) / height;
                        projection.Matrix = new float[] {2/w,   0,   0, tx,
                                                             0  , 2/h,   0, ty,
                                                             0  ,   0,  -1,  0,
                                                             0  ,   0,   0,  1};
                        return projection;
                    }
                    case Camera.Generic: {
                        projection.Set (generic);
                        return projection;
                    }
                    default: return projection;
                }
            }
        }


        public void SetParallel (float fovy, float aspectRatio, float near, float far) {
            if (fovy <= 0) {
                throw new ArgumentException ("Fov must be positive number, fovy=" + fovy);
            }
            if (aspectRatio <= 0) {
                throw new ArgumentException ("AspectRatio must positive number, ratio=" + aspectRatio);
            }
            this.mode = Camera.Parallel;
            this.fovy = fovy;
            this.aspectRation = aspectRatio;
            this.near = near;
            this.far = far;
        }

        public void SetPerspective (float fovy, float aspectRatio, float near, float far) {
            if (fovy <= 0 || fovy >= 180) {
                throw new ArgumentException ("Fovy must be within [0,180], fovy=" + fovy);
            }
            if (aspectRatio <= 0) {
                throw new ArgumentException ("AspectRatio must be positive number, ratio=" + aspectRatio);
            }
            if (near <= 0) {
                throw new ArgumentException ("Near must be positive number, near=" + near);
            }
            if (far <= 0) {
                throw new ArgumentException ("Far must be positive number, far=" + far);
            }

            this.mode = Camera.Perspective;
            this.fovy = fovy;
            this.aspectRation = aspectRatio;
            this.near = near;
            this.far = far;

        }

        public void SetScreen (float x, float y, float width, float height) {
            if (width <= 0) {
                throw new ArgumentException ("Width must be positive number, width=" + width);
            }
            if (height <= 0) {
                throw new ArgumentException ("Height must be positive number height=" + height);
            }

            this.mode = Camera.Screen;
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
        }

        public void SetGeneric (Transform transform) {
            if (transform == null) {
                throw new ArgumentNullException ("Transform is null");
            }

            this.mode = Generic;
            this.generic.Set (transform);
        }

        public override int Animate (int time) {

            base.Animate (time);

            var animatedFieldOfView  = new float[1];
            var animatedNearDistance = new float[1];
            var animatedFarDistance  = new float[1];
            var value                = new float[1];
            var animated             = new Dictionary<int, bool> (3);

            for (var i=0; i < AnimationTrackCount; i++) {
                var track = AnimationTrack[i];
                var ch    = AnimationTrackChannel[i];
                if (track.Controller == null || !track.Controller.Active (time)) {
                    continue;
                }
                var property = track.TargetProperty;
                var weight   = track.Controller.Weight;

                animated[property] = true;
                switch (property) {
                    case M3G.AnimationTrack.FieldOfView: {
                        track.Sample (time, ch, value);
                        animatedFieldOfView[0] = value[0] * weight;
                        break;
                    }
                    case M3G.AnimationTrack.NearDistance: {
                        track.Sample (time, ch, value);
                        animatedNearDistance[0] = value[0] * weight;
                        break;
                    }
                    case M3G.AnimationTrack.FarDistance: {
                        track.Sample (time, ch, value);
                        animatedFarDistance[0] = value[0] * weight;
                        break;
                    }
                }// switch
            }  // for


            if (animated[M3G.AnimationTrack.FieldOfView]) {
                this.fovy = animatedFieldOfView[0];
            }
            if (animated[M3G.AnimationTrack.NearDistance]) {
                this.near = animatedNearDistance[0];
            }
            if (animated[M3G.AnimationTrack.FarDistance]) {
                this.far = animatedFarDistance[0];
            }

            return 0;
        }

        public override Object3D Duplicate () {
            var cam = new Camera (this);
            cam.parent = null;
            return cam;
        }
        #endregion

    }
}
